#include "FileDocument.h"

#include "CodeDocumentLayout.h"
#include "CodeEditor.h"

#include <QtCore/QFile>
#include <QtCore/QDir>
#include <QtCore/QFileInfo>
#include <QtCore/QTextStream>
#include <QtCore/QTextCodec>
#include <QtGui/QMainWindow>
#include <QtGui/QSyntaxHighlighter>
#include <QtGui/QApplication>

using namespace CodeEdit;

//! @class DocumentMarker
DocumentMarker::DocumentMarker(QTextDocument *doc)
    : ITextMarkable(doc)
    , _document(doc)
{
}

bool DocumentMarker::addMark(CodeEdit::ITextMark *mark, int line)
{
    if (line >= 1) {
        int blockNumber = line - 1;
        CodeDocumentLayout *docLayout = qobject_cast<CodeDocumentLayout*>(_document->documentLayout());
        if (!docLayout) {
            return false;
        }
        QTextBlock block = _document->findBlockByNumber(blockNumber);
        if (block.isValid()) {
            TextBlockUserData *userData = CodeDocumentLayout::userData(block);
            userData->addMark(mark);
            mark->updateLineNumber(blockNumber + 1);
            mark->updateBlock(block);
            docLayout->_hasMarks = true;
            docLayout->requestUpdate();
            return true;
        }
    }
    return false;
}

CodeEdit::TextMarks DocumentMarker::marksAt(int line) const
{
    if (line >= 1) {
        int blockNumber = line - 1;
        QTextBlock block = _document->findBlockByNumber(blockNumber);
        if (block.isValid()) {
            if (TextBlockUserData *userData = CodeDocumentLayout::testUserData(block))
                return userData->marks();
        }
    }
    return TextMarks();
}

void DocumentMarker::removeMark(CodeEdit::ITextMark *mark)
{
    bool needUpdate = false;
    QTextBlock block = _document->begin();
    while (block.isValid()) {
        if (TextBlockUserData *data = static_cast<TextBlockUserData *>(block.userData())) {
            needUpdate |= data->removeMark(mark);
        }
        block = block.next();
    }
    if (needUpdate)
        updateMark(0);
}

bool DocumentMarker::hasMark(CodeEdit::ITextMark *mark) const
{
    QTextBlock block = _document->begin();
    while (block.isValid()) {
        if (TextBlockUserData *data = static_cast<TextBlockUserData *>(block.userData())) {
            if (data->hasMark(mark))
                return true;
        }
        block = block.next();
    }
    return false;
}

void DocumentMarker::updateMark(ITextMark *mark)
{
    Q_UNUSED(mark)
    CodeDocumentLayout *docLayout = qobject_cast<CodeDocumentLayout*>(_document->documentLayout());
    if (docLayout) {
        docLayout->requestUpdate();
    }
}

//! @class FileDocument
FileDocument::FileDocument()
    : _document(new QTextDocument(this))
    , _documentMarker(new DocumentMarker(_document))
    , _highlighter(0)
    , _lineTerminatorMode(NativeLineTerminator)
    , _codec(QTextCodec::codecForLocale())
    , _fileIsReadOnly(false)
    , _isBinaryData(false)
    , _hasDecodingError(false)
{
    QSettings settings;
    QTextCodec *candidate = QTextCodec::codecForName(settings
        .value(QLatin1String("General/DefaultFileEncoding")).toByteArray());
    if (candidate)
        _codec = candidate;
}

FileDocument::~FileDocument()
{
    QTextBlock block = _document->begin();
    while (block.isValid()) {
        if (TextBlockUserData *data = static_cast<TextBlockUserData *>(block.userData()))
            data->documentClosing();
        block = block.next();
    }
    delete _document;
    _document = 0;
}

QString FileDocument::mimeType() const
{
    return _mimeType;
}

void FileDocument::setMimeType(const QString &mt)
{
    _mimeType = mt;
}

/**
 * Saves the document to file.
 */
bool FileDocument::save(const QString &fileName)
{
    // Do tidy work before saving
    QTextCursor cursor(_document);
    cursor.beginEditBlock();
    if (_storageSettings.cleanWhitespace)
        cleanWhitespace(cursor, _storageSettings.cleanIndentation, _storageSettings.inEntireDocument);
    if (_storageSettings.addFinalNewLine)
        ensureFinalNewLine(cursor);
    cursor.endEditBlock();

    // Prepare for saving: file name, file handle, buffer, etc.
    QString fName = _fileName;
    if (!fileName.isEmpty())
        fName = fileName;

    QFile file(fName);
    if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate))
        return false;

    QString plainText = _document->toPlainText();

    // Ensure buffer contains the right line terminators
    if (_lineTerminatorMode == CRLFLineTerminator)
        plainText.replace(QLatin1Char('\n'), QLatin1String("\r\n"));

    // Write contents
    file.write(_codec->fromUnicode(plainText));
    if (!file.flush())
        return false;
    file.close();

    const QFileInfo fi(fName);
    _fileName = QDir::cleanPath(fi.absoluteFilePath());

    _document->setModified(false);
    emit titleChanged(fi.fileName());
    emit changed();

    _isBinaryData = false;
    _hasDecodingError = false;
    _decodingErrorSample.clear();

    return true;
}

/**
 * Renames associated fileName.
 */
void FileDocument::rename(const QString &newName)
{
    const QFileInfo fi(newName);
    _fileName = QDir::cleanPath(fi.absoluteFilePath());
    emit titleChanged(fi.fileName());
    emit changed();
}

/**
 * Returns true if the document is binary or has decoding error or explicitly
 * set as readonly.
 */
bool FileDocument::isReadOnly() const
{
    if (_isBinaryData || _hasDecodingError)
        return true;
    if (_fileName.isEmpty()) // have no corresponding file, so editing is ok
        return false;
    return _fileIsReadOnly;
}

/**
 * Returns true if the contents of the document is modified.
 */
bool FileDocument::isModified() const
{
    return _document->isModified();
}

/**
 * Checks whether the associated file is writable, if not, set _fileIsReadOnly to true.
 */
void FileDocument::checkPermissions()
{
    bool previousReadOnly = _fileIsReadOnly;
    if (!_fileName.isEmpty()) {
        const QFileInfo fi(_fileName);
        _fileIsReadOnly = !fi.isWritable();
    } else {
        _fileIsReadOnly = false;
    }
    if (previousReadOnly != _fileIsReadOnly)
        emit changed();
}

/**
 * Loads the document from file.
 */
bool FileDocument::open(const QString &fileName)
{
    QString title = tr("untitled");
    if (!fileName.isEmpty()) {
        const QFileInfo fi(fileName);
        _fileIsReadOnly = !fi.isWritable();
        _fileName = QDir::cleanPath(fi.absoluteFilePath());

        QFile file(fileName);
        if (!file.open(QIODevice::ReadOnly))
            return false;

        title = fi.fileName();

        QByteArray buf = file.readAll();
        /* Detect text codec via BOM.
         * codecForUtfText() tries to detect the encoding of the provided snippet
         * by checking the BOM.
         * It knows the utf encodings: utf32-be, utf32-le, utf16-be, utf16-le, utf8.
         * If no BOM is found, degrades to the ansi locale encoding.
         * todo: guess encoding from contents. (especially utf8-no-bom).
         */
        _codec = QTextCodec::codecForUtfText(buf, QTextCodec::codecForLocale());

        // Verify decoding error
        QString text = _codec->toUnicode(buf);
        QByteArray verifyBuf = _codec->fromUnicode(text); // slow
        // the minSize trick lets us ignore unicode headers
        int minSize = qMin(verifyBuf.size(), buf.size());
        _hasDecodingError = (minSize < buf.size()- 4
                          || memcmp(verifyBuf.constData() + verifyBuf.size() - minSize,
                                    buf.constData() + buf.size() - minSize, minSize));

        if (_hasDecodingError) {
            int p = buf.indexOf('\n', 16384);
            if (p < 0)
                _decodingErrorSample = buf;
            else
                _decodingErrorSample = buf.left(p);
        } else {
            _decodingErrorSample.clear();
        }

        // Detect line terminator mode
        int lf = text.indexOf('\n');
        if (lf > 0 && text.at(lf-1) == QLatin1Char('\r')) {
            _lineTerminatorMode = CRLFLineTerminator;
        }
        else if (lf >= 0) {
            _lineTerminatorMode = LFLineTerminator;
        }
        else {
            _lineTerminatorMode = NativeLineTerminator;
        }

        _document->setModified(false);
        if (_isBinaryData)
            _document->setHtml(tr("<em>Binary data</em>"));
        else
            _document->setPlainText(text);
        CodeDocumentLayout *docLayout = qobject_cast<CodeDocumentLayout*>(_document->documentLayout());
        if (!docLayout) {
            return true;
        }

        docLayout->_lastSaveRevision = _document->revision();
        _document->setModified(false);

        emit titleChanged(title);
        emit changed();
    }
    return true;
}

/**
 * @public
 * Reloads document with the specifed text codec.
 */
void FileDocument::reload(QTextCodec *codec)
{
    if (codec) {
        _codec = codec;
        reload();
    }
}

/// @public
void FileDocument::reload()
{
    emit aboutToReload();
    if (open(_fileName))
        emit reloaded();
}

/// @public
Core::IFile::ReloadBehavior FileDocument::reloadBehavior(ChangeTrigger state, ChangeType type) const
{
    if (type == TypePermissions)
        return BehaviorSilent;
    if (type == TypeContents) {
        if (state == TriggerInternal && !isModified())
            return BehaviorSilent;
        return BehaviorAsk;
    }
    return BehaviorAsk;
}

/// @public
void FileDocument::reload(ReloadFlag flag, ChangeType type)
{
    if (flag == FlagIgnore)
        return;
    if (type == TypePermissions) {
        checkPermissions();
    } else {
        reload();
    }
}

/**
 * @public
 * Changes syntax highlighter.
 */
void FileDocument::setSyntaxHighlighter(QSyntaxHighlighter *highlighter)
{
    if (_highlighter)
        delete _highlighter;
    _highlighter = highlighter;
    if (_highlighter) {
        _highlighter->setParent(this);
        _highlighter->setDocument(_document);
    }
}

/// @public
void FileDocument::cleanWhitespace(const QTextCursor &cursor)
{
    bool hasSelection = cursor.hasSelection();
    QTextCursor copyCursor = cursor;
    copyCursor.beginEditBlock();
    cleanWhitespace(copyCursor, true, true);
    if (!hasSelection)
        ensureFinalNewLine(copyCursor);
    copyCursor.endEditBlock();
}

/// @private
void FileDocument::cleanWhitespace(QTextCursor& cursor, bool cleanIndentation, bool inEntireDocument)
{
    CodeDocumentLayout *docLayout = qobject_cast<CodeDocumentLayout*>(_document->documentLayout());

    QTextBlock block = _document->findBlock(cursor.selectionStart());
    QTextBlock end;
    if (cursor.hasSelection())
        end = _document->findBlock(cursor.selectionEnd()-1).next();

    while (block.isValid() && block != end) {
        if (inEntireDocument || block.revision() != docLayout->_lastSaveRevision) {
            QString blockText = block.text();
            if (int trailing = _tabSettings.trailingWhitespaces(blockText)) {
                cursor.setPosition(block.position() + block.length() - 1);
                cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor, trailing);
                cursor.removeSelectedText();
            }
            if (cleanIndentation && !_tabSettings.isIndentationClean(block)) {
                cursor.setPosition(block.position());
                int firstNonSpace = _tabSettings.firstNonSpace(blockText);
                if (firstNonSpace == blockText.length()) {
                    cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
                    cursor.removeSelectedText();
                } else {
                    int column = _tabSettings.columnAt(blockText, firstNonSpace);
                    cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, firstNonSpace);
                    QString indentationString = _tabSettings.indentationString(0, column, block);
                    cursor.insertText(indentationString);
                }
            }
        }

        block = block.next();
    }
}

/// @private
void FileDocument::ensureFinalNewLine(QTextCursor& cursor)
{
    cursor.movePosition(QTextCursor::End, QTextCursor::MoveAnchor);
    bool emptyFile = !cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor);

    if (!emptyFile && cursor.selectedText().at(0) != QChar::ParagraphSeparator) {
        cursor.movePosition(QTextCursor::End, QTextCursor::MoveAnchor);
        cursor.insertText(QLatin1String("\n"));
    }
}
